/* Copyright (C) 2001, 2009 United States Government as represented by
the Administrator of the National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.formats.shapefile;

import com.sun.opengl.util.BufferUtil;
import gov.nasa.worldwind.geom.Sector;
import gov.nasa.worldwind.util.*;

import java.awt.geom.*;
import java.io.IOException;
import java.nio.*;
import java.nio.channels.ReadableByteChannel;
import java.util.*;

/**
 * Utilities for working with shapefiles.
 *
 * @author Patrick Murris
 * @version $Id: ShapefileUtils.java 13118 2010-02-14 21:15:07Z tgaskins $
 */
public class ShapefileUtils
{
    /**
     * Normalizes a specified value to the range [-180, 180].
     *
     * @param longitudeDegrees the value to normalize.
     *
     * @return the normalized value.
     */
    public static double normalizeLongitude(double longitudeDegrees)
    {
        while (longitudeDegrees < -180)
        {
            longitudeDegrees += 360;
        }
        while (longitudeDegrees > 180)
        {
            longitudeDegrees -= 360;
        }

        return longitudeDegrees;
    }

    /**
     * Normalize the min-X and max-X values of a rectangle to the range [-180, 180].
     *
     * @param rect the rectangle to normalize.
     *
     * @return the input rectangle with X values normalized.
     *
     * @throws IllegalArgumentException if the specified rectangle reference is null.
     */
    public static Rectangle2D normalizeRectangle(Rectangle2D rect)
    {
        if (rect == null)
        {
            String message = Logging.getMessage("nullValue.RectangleIsNull");
            Logging.logger().log(java.util.logging.Level.SEVERE, message);
            throw new IllegalArgumentException(message);
        }

        if (rect.getMinX() >= -180 && rect.getMaxX() <= 180)
            return rect;

        rect.setRect(-180, rect.getY(), 360, rect.getHeight());
        return rect;
    }

    // *** I/O ***

    // TODO: Replace the next two methods with use of the WWIO equivalents.

    public static ByteBuffer readByteChannelToBuffer(ReadableByteChannel channel, int numBytes) throws IOException
    {
        return readByteChannelToBuffer(channel, numBytes, null);
    }

    public static ByteBuffer readByteChannelToBuffer(ReadableByteChannel channel, int numBytes,
        ByteBuffer buffer) throws IOException
    {
        if (buffer == null)
            buffer = ByteBuffer.allocate(numBytes);

        int bytesRead = 0;
        int count = 0;
        while (count >= 0 && (numBytes - bytesRead) > 0)
        {
            count = channel.read(buffer);
            if (count > 0)
            {
                bytesRead += count;
            }
        }

        buffer.flip();
        buffer.order(ByteOrder.LITTLE_ENDIAN); // Default to least significant byte first order.

        return buffer;
    }

    /**
     * Reads and returns an array of integers from a byte buffer.
     *
     * @param buffer     the byte buffer to read from.
     * @param numEntries the number of integers to read.
     *
     * @return the integers read.
     *
     * @throws IllegalArgumentException if the specified buffer reference is null.
     */
    public static int[] readIntArray(ByteBuffer buffer, int numEntries)
    {
        if (buffer == null)
        {
            String message = Logging.getMessage("nullValue.InputBufferIsNull");
            Logging.logger().severe(message);
            throw new IllegalArgumentException(message);
        }

        int[] array = new int[numEntries];
        for (int i = 0; i < numEntries; i++)
        {
            array[i] = buffer.getInt();
        }

        return array;
    }

    /**
     * Reads and returns an array of doubles from a byte buffer.
     *
     * @param buffer     the byte buffer to read from.
     * @param numEntries the number of doubles to read.
     *
     * @return the doubles read.
     *
     * @throws IllegalArgumentException if the specified buffer reference is null.
     */
    public static double[] readDoubleArray(ByteBuffer buffer, int numEntries)
    {
        if (buffer == null)
        {
            String message = Logging.getMessage("nullValue.InputBufferIsNull");
            Logging.logger().severe(message);
            throw new IllegalArgumentException(message);
        }

        double[] array = new double[numEntries];
        for (int i = 0; i < numEntries; i++)
        {
            array[i] = buffer.getDouble();
        }

        return array;
    }

    /**
     * Copy a record's X-Y points from a byte buffer to a vector buffer.
     *
     * @param from      the buffer to copy from.
     * @param to        the buffer to copy to.
     * @param position  the position in the output buffer at which to place the values.
     * @param numPoints the number of X-Y points to copy. No points are copied if the value is less than or equal to
     *                  zero.
     *
     * @throws IllegalArgumentException if either the input or output buffer reference is null, or if the specified
     *                                  position is invalid.
     */
    public static void transferPoints(ByteBuffer from, VecBuffer to, int position, int numPoints)
    {
        if (from == null)
        {
            String message = Logging.getMessage("nullValue.InputBufferIsNull");
            Logging.logger().severe(message);
            throw new IllegalArgumentException(message);
        }

        if (to == null)
        {
            String message = Logging.getMessage("nullValue.OutputBufferIsNull");
            Logging.logger().severe(message);
            throw new IllegalArgumentException(message);
        }

        if (numPoints <= 0)
            return;

        DoubleBuffer doubleBuffer = from.slice().order(ByteOrder.LITTLE_ENDIAN).asDoubleBuffer();
        doubleBuffer.limit(numPoints * 2);
        VecBuffer vecBuffer = new VecBuffer(2, new BufferWrapper.DoubleBufferWrapper(doubleBuffer));
        to.putSubBuffer(position, vecBuffer);

        from.position(from.position() + 2 * 8 * numPoints);
    }

    // *** Record selection ***

    /**
     * Selects shapefile records for which a given attribute match a given value.
     *
     * @param records         the shapefile record list.
     * @param attributeName   the name of the attribute that should match the given value.
     * @param value           the value the attribute should match.
     * @param acceptNullValue if true the filtering process will accept records which do not have the given attribute.
     *
     * @return the filtered record list or <code>null</code> if the provided record list is empty.
     */
    public static List<ShapefileRecord> selectRecords(List<ShapefileRecord> records, String attributeName,
        Object value, boolean acceptNullValue)
    {
        Iterator<ShapefileRecord> iter = records.iterator();
        if (!iter.hasNext())
            return null;

        ShapefileRecord record;
        while (iter.hasNext())
        {
            record = iter.next();
            if (record == null)
                iter.remove();
            else
            {
                Object o = record.getAttributes().getValue(attributeName);
                if ((o == null && !acceptNullValue)
                    || (o != null && o instanceof String && !((String) o).equalsIgnoreCase((String) value))
                    || (o != null && !o.equals(value)))

                    iter.remove();
            }
        }

        return records;
    }

    /**
     * Selects shapefile records that are contained in or intersect with a given {@link Sector}.
     *
     * @param records the shapefile record list.
     * @param sector  the geographic sector that records must be contained in or intersect with.
     *
     * @return the filtered record list or <code>null</code> if the provided record list is empty.
     */
    public static List<ShapefileRecord> selectRecords(List<ShapefileRecord> records, Sector sector)
    {
        Iterator<ShapefileRecord> iter = records.iterator();
        if (!iter.hasNext())
            return null;

        Rectangle2D rectangle = sector.toRectangleDegrees();
        ShapefileRecord record;
        while (iter.hasNext())
        {
            record = iter.next();
            if (record == null)
                iter.remove();
            else
            {
                if (record instanceof ShapefileRecordPoint)
                {
                    double[] point = ((ShapefileRecordPoint) record).getPoint();
                    if (!rectangle.contains(point[0], point[1]))
                        iter.remove();
                }
                else if (record instanceof ShapefileRecordPolyline)  // catches polygons too
                {
                    if (!rectangle.intersects(((ShapefileRecordPolyline) record).getBoundingRectangle()))
                        iter.remove();
                }
            }
        }

        return records;
    }

    /**
     * Returns a new {@link CompoundVecBuffer} that only contains the selected records sub-buffers or parts. Records are
     * selected when the specified attribute has the given value. Note that the original backing {@link VecBuffer} is
     * not duplicated in the process.
     * <p/>
     * String values are not case sensitive while attributes names are.
     *
     * @param shapeFile     the shapefile to select records from.
     * @param attributeName the name of the attribute which value is to be compared to the given value.
     * @param value         the value to compare the attribute with.
     *
     * @return a new {@link CompoundVecBuffer} that only contains the selected records sub-buffers or parts.
     */
    public static CompoundVecBuffer createBufferFromAttributeValue(Shapefile shapeFile, String attributeName,
        Object value)
    {
        List<ShapefileRecord> records = new ArrayList<ShapefileRecord>(shapeFile.getRecords());
        // Filter record list
        ShapefileUtils.selectRecords(records, attributeName, value, false);

        return ShapefileUtils.createBufferFromRecords(records);
    }

    /**
     * Returns a new {@link CompoundVecBuffer} that only contains the given records sub-buffers or parts. Note that the
     * original backing {@link VecBuffer} is not duplicated in the process.
     *
     * @param records a list of records to include in the buffer.
     *
     * @return a new {@link CompoundVecBuffer} that only contains the given records sub-buffers or parts.
     */
    public static CompoundVecBuffer createBufferFromRecords(List<ShapefileRecord> records)
    {
        if (records.size() == 0)
            return null;

        // Count parts
        int numParts = 0;
        for (ShapefileRecord record : records)
        {
            numParts += record.getNumberOfParts();
        }

        // Get source geometry buffer
        CompoundVecBuffer sourceBuffer = records.get(0).getShapeFile().getBuffer();

        // Create new offset and length buffers
        IntBuffer offsetBuffer = BufferUtil.newIntBuffer(numParts);
        IntBuffer lengthBuffer = BufferUtil.newIntBuffer(numParts);
        for (ShapefileRecord record : records)
        {
            for (int part = 0; part < record.getNumberOfParts(); part++)
            {
                offsetBuffer.put(sourceBuffer.getSubPositionBuffer().get(record.getFirstPartNumber() + part));
                lengthBuffer.put(sourceBuffer.getSubLengthBuffer().get(record.getFirstPartNumber() + part));
            }
        }
        offsetBuffer.rewind();
        lengthBuffer.rewind();

        return new CompoundVecBuffer(sourceBuffer.getBackingBuffer(), offsetBuffer, lengthBuffer, numParts,
            sourceBuffer.getBufferFactory());
    }
}
